import babelGenerator from '@babel/generator'
import { parse } from '@babel/parser'
import traverse from '@babel/traverse'
import * as t from '@babel/types'
import chalk from 'chalk'
import { spawn } from 'child_process'
import fs from 'fs-extra'
import { once, uniqueId } from 'lodash-es'
import path from 'path'
import { PluginContext, PluginHooks } from 'rollup'
import which from 'which'
import * as pkg from '../../package.json'
import { COMMONJS_REQUIRE, CWD } from '../constants'
import { EmscriptenModuleConfig } from '../types'
import { toArray } from '../utils'
import { ResolvedOptions } from './../options'
import { getSourcePath, hashString } from './../utils'
import { getHelper as getBaseHelper } from './base'
import emscriptenModule from './modules/emscripten'
import wasmLoaderHelper from './modules/wasm'
import { generageImports } from './modules/_utils'
import { defineHelper } from './_utils'

const _quoteString = (str: string) => {
  const id = uniqueId('quote')
  return `(?<${id}>['"])${str.replace(/\s/g, '[$1]')}\\k<${id}>`
}

const _createReplacer = (
  replaceRules: Record<string, string | ((groups: Record<string, string>) => string)>
) => {
  const replacements: Record<string, string | ((groups: Record<string, string>) => string)> =
    Object.create(null)
  const re = new RegExp(
    `(?:${Object.entries(replaceRules)
      .map(([rule, replaceValue]) => {
        const key = uniqueId('rule')
        replacements[key] = replaceValue
        rule = rule
          .replace(/(?<!\\)\$/g, '\\$')
          .replace(/(?<![^\\]\[)(?<l>[\w\$])\s+(?<r>[^\w\$])(?![^\\]\])/g, '$<l>\\s*$<r>')
          .replace(/(?<![^\\]\[)(?<l>[^\w\$])\s+(?<r>[\w\$])(?![^\\]\])/g, '$<l>\\s*$<r>')
          .replace(/(?<![^\\]\[)(?<l>[^\w\$])\s+(?<r>[^\w\$])(?![^\\]\])/g, '$<l>\\s*$<r>')
          .replace(/\<TAB\>/g, uniqueId('tab'))
          .replace(/\[(.)\]/, '$1')
        if (/^[\$\w]/.test(rule)) rule = '(?<![\\w$])' + rule
        if (/[\$\w]$/.test(rule)) rule = rule + '(?![\\w$])'

        return `(?<${key}>${rule})`
      })
      .join('|')})`,
    'g'
  )

  return (code: string) => {
    return code.replace(re, (...args) => {
      const len = args.length
      const groups = args[len - 1]
      if (typeof groups === 'object') {
        for (const key in groups) {
          if (groups[key] && key in replacements) {
            let replacement = replacements[key]
            if (typeof replacement === 'function') replacement = replacement(groups)
            return replacement
          }
        }
        return args[len - 2]
      } else {
        return groups
      }
    })
  }
}

export const resolveEmccConfig = (config: Partial<EmscriptenModuleConfig>) => {
  let {
    input = [],
    includes = [],
    libDirs = [],
    libs = [],
    jsTransfrom = [],
    jsLibrary = [],
    defines = {},
    compilerArgv = [],
  } = config

  return [
    ...compilerArgv,
    ...toArray<string>(input).filter(Boolean),
    ...toArray<string>(includes)
      .filter(Boolean)
      .map((i) => `-I${i}`),
    ...toArray<string>(libDirs)
      .filter(Boolean)
      .map((l) => `-L${l}`),
    ...toArray<string>(libs)
      .filter(Boolean)
      .map((l) => `-l${l}`),
    ...toArray<string>(jsLibrary)
      .filter(Boolean)
      .map((j) => ['--js-library', j]),
    ...toArray<string>(jsTransfrom)
      .filter(Boolean)
      .map((j) => ['--js-transform', j]),
    ...Object.entries(defines).map(([d, v]) => (v ? `-D${d}=${v}` : `-D${d}`)),
  ]
    .flat()
    .filter((s) => s && typeof s === 'string')
}
const getCompiler = async (options: ResolvedOptions['emscripten']) => {
  const emcc = which.sync('emcc', { nothrow: true })

  const dir = path.resolve(CWD, 'node_modules/.rollup-plugin-wasm/emscripten')
  await fs.mkdirs(dir)

  if (
    emcc &&
    (await new Promise<boolean>((resolve, reject) => {
      const emccVersionProccess = spawn(emcc, ['-v'])
      let stdout = ''
      let stderr = ''

      emccVersionProccess.on('error', () => {
        resolve(false)
      })

      emccVersionProccess.on('spawn', () => {
        setTimeout(() => {
          emccVersionProccess.kill('SIGKILL')
        }, 500)
      })

      emccVersionProccess.stdout.on('data', (chunk) => {
        stdout += chunk.toString()
      })

      emccVersionProccess.stderr.on('data', (chunk) => {
        stderr += chunk.toString()
      })

      emccVersionProccess.on('exit', () => {
        const out = stdout + '\n' + stderr
        const re =
          /\bemcc .+?(?<version>\d+\.\d+\.\d+)(?:.+?\bclang version\s+(?<clang_version>\d+\.\d+\.\d+))/s

        const match = out.match(re)

        if (match) {
          resolve(true)
          console.info(
            chalk.yellow`[${pkg.name}]: Found emscripten-tools. Version is ${match.groups?.version}(clang version: ${match.groups?.clang_version}).`
          )
        } else {
          resolve(false)
        }
      })
    }))
  ) {
    return (config: EmscriptenModuleConfig) => {
      return new Promise<{
        name: string
        jsFileName: string
        wasmFileName: string
      }>(async (resolve, reject) => {
        try {
          const inputs = toArray(config.input)
          const name =
            (path.parse(config.id).name || 'emscripten') +
            '-' +
            hashString(inputs.join(''), 'hex', 8)
          const jsFileName = path.join(dir, name + '.mjs')
          const wasmFileName = path.join(dir, name + '.wasm')

          await Promise.all([jsFileName, wasmFileName])

          const argv = [
            ...options.argv,
            ...resolveEmccConfig(config),
            '--minify',
            '0',
            '--closure',
            '0',
            '--bind',
            '-o',
            jsFileName,
          ]

          const emccProcess = spawn(emcc, argv, {
            cwd: CWD,
          })
          let stderr = ''
          emccProcess.stderr.on('data', (chunk) => {
            stderr += chunk.toString()
          })
          emccProcess.on('exit', async (code, signal) => {
            if (signal || code) {
              console.error(
                chalk.red`✖ Emscripten-module "${config.id}" compiled failure: ${stderr}`
              )
              return reject(`Compiler exited abnormally. code: ${code}, signal: ${signal}.`)
            }
            console.info(chalk.yellow`✔ Emscripten-module "${config.id}" compiled successfully.`)
            resolve({
              name,
              jsFileName,
              wasmFileName,
            })
          })
          emccProcess.on('spawn', () =>
            console.info(chalk.blue`⭐ Start compiling emscripten-module "${config.id}".`)
          )
          emccProcess.on('error', (err) => {
            console.error(
              chalk.red`✖ Emscripten-module "${config.id}" compiled failure: ${err?.toString()}`
            )
            reject(err)
          })
        } catch (error) {
          reject(error)
        }
      })
    }
  }

  console.error(chalk.red`[${pkg.name}]: Emscripten-tools not found. Did you install it?`)
}

const _replaceRules: Record<string, string | ((groups: Record<string, string>) => string)> = {
  [`typeof WebAssembly !==? ${_quoteString('object')}`]: 'false',
  [`typeof WebAssembly ===? ${_quoteString('undefined')}`]: 'false',
  [`var Module = typeof Module !==? ${_quoteString('undefined')} \\? Module : \\{ \\}`]: '',
  [`getBinaryPromise \\( \\) \\. then \\(`]: `Promise.resolve(Module["wasmBinary"]||_$_getWasmBinaryPromise()).then(`,
  [`function instantiateAsync \\( \\) \\{`]: `function instantiateAsync() {return instantiateArrayBuffer(receiveInstantiationResult) `,
  [`if \\( ! Object \\. getOwnPropertyDescriptor \\( Module , ${_quoteString(
    '[\\$\\w]+'
  )} \\) \\)`]: `if(false)`,
  [`export default`]: '',
  [`function \\( Module \\) \\{`]: `function ( Module, _$_getWasmBinaryPromise, Promise, _$_createInstanceFromBuffer ) {`,
  [`WebAssembly \\. instantiate`]: '(_$_createInstanceFromBuffer)',
}

const _removeAssignmentVars = emscriptenModule.exports as readonly string[]
const _removeUsedVars = ['scriptDirectory', '_scriptDir', 'wasmBinaryFile', 'wasmBinary']
const _replace = _createReplacer(_replaceRules)

export const getHelper = defineHelper((options) => {
  const _getCompiler = once(() => getCompiler(options.emscripten))
  const utilsHelper = getBaseHelper(options)

  const loadEmscriptenModule = async (
    context: PluginContext,
    id: string,
    jsFileName: string,
    wasmFileName: string
  ) => {
    let [code, source] = await Promise.all([
      fs.readFile(jsFileName).then((buffer) => _replace(buffer.toString())),
      fs.readFile(wasmFileName),
    ])

    const inline = options.inline(wasmFileName, id)

    code =
      `import { SynchronousPromise as _$SynchronousPromise } from 'synchronous-promise';` +
      generageImports(
        [emscriptenModule, emscriptenModule.exports],
        [
          wasmLoaderHelper,
          {
            WebAssembly_instantiateBuffer: '_$WebAssembly_instantiateBuffer',
            WebAssembly_instantiateBufferSync: '_$WebAssembly_instantiateBufferSync',
          },
        ]
      ) +
      (inline
        ? `
        ${generageImports([utilsHelper.getInlineBufferModule(source), [], '_$binary'])}
        const _$getWasmBinary = () => { return _$binary };
        `
        : generageImports([
            await utilsHelper.getReadFileBufferModule(context, wasmFileName, source, 'both'),
            { sync_read: '_$getWasmBinarySync', async_read: '_$getWasmBinaryASync' },
          ])) +
      code +
      (inline
        ? `
          const _$instantiate = async (_Module) => { return await Module(_Module, _$getWasmBinary , Promise, _$WebAssembly_instantiateBuffer) };
          const _$instantiateSync = (_Module) => { let _instance; Module(_Module, _$getWasmBinary , _$SynchronousPromise, _$WebAssembly_instantiateBufferSync).then((instance) => { _instance = instance }); return _instance  };
        `
        : `
          const _$instantiate = async (_Module) => { return await Module(_Module, _$getWasmBinaryASync , Promise, _$WebAssembly_instantiateBuffer) };
          const _$instantiateSync = (_Module) => { let _instance; Module(_Module, _$getWasmBinarySync , _$SynchronousPromise, _$WebAssembly_instantiateBufferSync).then((instance) => { _instance = instance }); return _instance  };
        `) +
      `
        const _$instantiateSyncTry = (_Module) => { try { return _$instantiateSync(_Module) } catch(err) { console.error(err) } };
        export default /*#__PURE__*/ _$instantiateSyncTry();
        export { _$instantiate as instantiate, _$instantiateSync as instantiateSync }
      `

    const ast = parse(code, {
      allowImportExportEverywhere: true,
      sourceType: 'module',
      plugins: ['importMeta'],
    })

    traverse(ast, {
      Import(path) {
        path.skip()
      },
      VariableDeclarator(path) {
        const {
          node: { id, init },
        } = path
        if (t.isIdentifier(id)) {
          let cnode
          if (_removeAssignmentVars.includes(id.name) || _removeUsedVars.includes(id.name)) {
            path.remove()
          } else if (
            id.name === 'Module' &&
            init &&
            t.isCallExpression(init) &&
            t.isFunctionExpression((cnode = init.callee)) &&
            init.arguments.length === 0
          ) {
            const body = cnode.body.body
            const [child0, child1] = body
            if (body.length === 2 && t.isReturnStatement(child1)) {
              const stateParent = path.getStatementParent()
              stateParent?.insertBefore(t.cloneNode(child0))
              path.node.init = child1.argument
            }
          }
        }
      },
      AssignmentExpression(path) {
        const {
          node: { left },
        } = path
        if (
          t.isIdentifier(left) &&
          (_removeAssignmentVars.includes(left.name) || _removeUsedVars.includes(left.name))
        ) {
          path.remove()
        }
      },
      FunctionDeclaration(path) {
        const {
          node: { id },
        } = path
        if (id && (_removeAssignmentVars.includes(id.name) || _removeUsedVars.includes(id.name))) {
          path.remove()
        }
      },
      ClassDeclaration(path) {
        const {
          node: { id },
        } = path
        if (id && (_removeAssignmentVars.includes(id.name) || _removeUsedVars.includes(id.name))) {
          path.remove()
        }
      },
      Identifier(path) {
        const { node, parentPath } = path
        const { name } = node

        if (!_removeUsedVars.includes(name)) return

        if (parentPath) {
          if (parentPath.isProperty()) {
            const {
              node: {
                value,
                //@ts-ignore
                computed,
                key,
              },
            } = parentPath
            if (computed && (key.type !== 'Identifier' || key.name !== name)) return
            if (!value || value.type !== 'Identifier' || value.name !== name) return
          }
          if (parentPath.isMemberExpression() || parentPath.isOptionalMemberExpression()) {
            const {
              node: { object, computed, property },
            } = parentPath
            if (computed && (property.type !== 'Identifier' || property.name !== name)) return
            if (object.type !== 'Identifier' || object.name !== name) return
          }
          if (parentPath.isMethod()) {
            const {
              node: { computed, key },
            } = parentPath
            if (computed && (key.type !== 'Identifier' || key.name !== name)) return
          }
        }
        const statement = path.getStatementParent()
        if (statement) {
          if (!statement.isFunction() && !statement.isClass()) {
            statement.replaceWith(t.emptyStatement())
            return
          }
        }
        path.replaceWith(t.identifier('undefined'))
      },
    })

    return babelGenerator(ast).code
  }

  const exts = [
    '.cpp',
    '.c++',
    '.cc',
    '.c',
    '.i',
    ...(process.platform === 'win32' ? ['.obj', '.lib'] : ['.o', '.a']),
  ]

  const EM_INTERNAL_PREFIX = 'WASM-EMSCRIPTEN:'
  const EM_INTERNAL_SUFFIX = '?emscripten'
  const EM_MODULE_RE = /^(?:emscripten|emcc):(.+)$/

  const baseModuleConfigs: Record<string, EmscriptenModuleConfig> = options.emscripten.modules.reduce(
    (obj, config) => {
      obj[config.id] = config
      return obj
    },
    Object.create(null)
  )

  const moduleConfigs: Record<string, EmscriptenModuleConfig> = Object.create(baseModuleConfigs)

  const generateBundle: PluginHooks['generateBundle'] = function () {
    const ownKeys = Reflect.ownKeys(moduleConfigs)
    for (const key of ownKeys) {
      if (typeof key === 'string') delete moduleConfigs[key]
    }
  }

  const resolveId: PluginHooks['resolveId'] = async function (id, importer, opts) {
    let match
    if (id in moduleConfigs) {
      return EM_INTERNAL_PREFIX + id + EM_INTERNAL_SUFFIX
    }
    if (id.endsWith(COMMONJS_REQUIRE)) {
      id = id.slice(0, id.length - COMMONJS_REQUIRE.length)
    }
    if (id in moduleConfigs) {
      return EM_INTERNAL_PREFIX + id + EM_INTERNAL_SUFFIX
    }

    if (id.startsWith(EM_INTERNAL_PREFIX) && id.endsWith(EM_INTERNAL_SUFFIX)) {
      return id
    }
    if ((match = id.match(EM_MODULE_RE))) {
      id = match[1]
      if (!(id in moduleConfigs)) {
        const result = await getSourcePath(this, id, importer, exts, opts)
        if (!result) return
        id = result
        moduleConfigs[id] = { id, input: id }
      }
      return EM_INTERNAL_PREFIX + id + EM_INTERNAL_SUFFIX
    }
  }

  const load: PluginHooks['load'] = async function (id) {
    const rawId = id
    if (id.startsWith(EM_INTERNAL_PREFIX) && id.endsWith(EM_INTERNAL_SUFFIX)) {
      id = id.slice(EM_INTERNAL_PREFIX.length)
      id = id.slice(0, id.length - EM_INTERNAL_SUFFIX.length)
      const compiler = await _getCompiler()
      const moduleConfig = moduleConfigs[id]

      if (!compiler || !moduleConfig) return

      utilsHelper.addWatchFile(this, rawId, ...toArray(moduleConfig.input))

      const result = await compiler(moduleConfig)
      if (result) {
        const { jsFileName, wasmFileName } = result
        return await loadEmscriptenModule(this, id, jsFileName, wasmFileName)
      }
    }
  }

  return {
    generateBundle,
    resolveId,
    load,
  }
})
